package com.ar4j.handler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import javax.sql.DataSource;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import net.sf.cglib.proxy.MethodProxy;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordContext;
import com.ar4j.ActiveRecordMetadata;
import com.ar4j.sql.ActiveRecordSqlParameterSource;
import com.ar4j.sql.SqlUtils;
import com.ar4j.util.ClassUtils;
import com.ar4j.util.CollectionUtils;

/**
 * A handler for active record create/update (save) operations
 */
public class SaveMethodHandler implements IMethodHandler {
  private String updateQuery;
  private ReentrantLock updateQueryLock = new ReentrantLock();
  
  private Map<DataSource, SimpleJdbcInsert> insertCache = Collections.synchronizedMap(new HashMap<DataSource, SimpleJdbcInsert>());
  private ReentrantLock insertCacheLock = new ReentrantLock();
  
  /**
   * Creates a new record if one did not exist, or updates a record with the data contained in the 
   * active record.
   */  
  @Override
  public Object intercept(ActiveRecordMetadata metadata, ActiveRecordContext context, Object target, Method method, Object[] args, MethodProxy proxy) 
    throws Throwable {
    
    // make sure we have a template first
    if(context.getSimpleTemplate() == null || context.getDataSource() == null) {
      throw new IllegalStateException("Cannot execute find on an active record that does not have a datasource set");
    }
    
    ActiveRecord<?> record = (ActiveRecord<?>) target;
    if(record.isNewRecord()) {
      Number key = create(record, metadata, context);
      Object id = ClassUtils.coerceValueToType(key, record.getIdentifierPropertyType());
      record.setProperty(record.getIdentifierPropertyName(), id);
    } else {
      update(record, metadata, context);
    }
    
    record.reload();
    return record;
  }
  
  /**
   * Perform a SQL insert with the active record data
   */ 
  private Number create(ActiveRecord<?> record, ActiveRecordMetadata metadata, ActiveRecordContext context) {
    
    // create the insert if needed
    SimpleJdbcInsert insert = insertCache.get(context.getDataSource());
    if(insert == null) {
      insertCacheLock.lock();
      try {
        insert = insertCache.get(context.getDataSource());
        if(insert == null) {
          insert = new SimpleJdbcInsert(context.getDataSource())
            .withTableName(metadata.getTableName())
            .usingGeneratedKeyColumns(getAutogeneratedColumns(metadata))
            .usingColumns(getNonIdentifierColumns(metadata));
          
          insertCache.put(context.getDataSource(), insert);
        }
      } finally {
        insertCacheLock.unlock();
      }
    }
    
    ActiveRecordSqlParameterSource parameters = new ActiveRecordSqlParameterSource(record, metadata);
    return insert.executeAndReturnKey(parameters);
  }
  
  /**
   * Perform a SQL update with the active record date, the identifier will never be updated.
   */ 
  private void update(ActiveRecord<?> record, ActiveRecordMetadata metadata, ActiveRecordContext context) {
    
    // compose update query if we are running for the first time
    if(updateQuery == null) {
      updateQueryLock.lock();
      try {
        if(updateQuery == null) {
          updateQuery = composeUpdateQuery(metadata);
        }
      } finally {
        updateQueryLock.unlock();
      }
    }
    
    ActiveRecordSqlParameterSource parameters = new ActiveRecordSqlParameterSource(record, metadata);
    context.getSimpleTemplate().update(updateQuery, parameters);
  }
    
  
  /**
   * @return an array of column names that will be autogenerated by the backing store
   */ 
  private String[] getAutogeneratedColumns(ActiveRecordMetadata metadata) {
    String[] out = new String[metadata.getPropertiesAutogeneratedInBackingStore().size()];
    int i = 0;
    for(Iterator<String> it  = metadata.getPropertiesAutogeneratedInBackingStore().iterator(); it.hasNext(); i++) {
      out[i] = metadata.getPropertyColumnNameMap().get(it.next());
    }
    
    return out;
  }
  
  /**
   * @return an array of column names for all non identifier properties for the given active record
   * metadata
   */ 
  private String[] getNonIdentifierColumns(ActiveRecordMetadata metadata) {
    String[] columns = new String[metadata.getBackingStoreWritableProperties().size() - 1];
    int i = 0;
    for(Iterator<String> it = metadata.getBackingStoreWritableProperties().iterator(); it.hasNext(); ) {
      String property = it.next();
      if(metadata.getIdentifierPropertyName().equals(property)) {
        continue;
      }
      
      String columnName = metadata.getPropertyColumnNameMap().get(property);
      columns[i] = columnName;
      i++;
    }
    
    return columns;
  }

  /**
   * Composes an actual SQL UPDATE statement with all non identifier properties of an active record.
   * The values required for this statement are provided via host bind parameters (':param') within
   * the statement and is compatible with spring's JdbcTemplate.update and preparement statement
   * operations.
   */ 
  private String composeUpdateQuery(ActiveRecordMetadata metadata) {
    StringBuilder sql = new StringBuilder();
    sql.append("UPDATE ");
    sql.append(metadata.getTableName());
    sql.append(" SET ");
    
    boolean first = true;
    for(String property : metadata.getBackingStoreWritableProperties()) {
      if(metadata.getIdentifierPropertyName().equals(property)) {
        continue;
      }
      
      if(!first) {
        sql.append(", ");
      } else {
        first = false;
      }
     
      sql.append(metadata.getPropertyColumnNameMap().get(property));
      sql.append(" = :");
      sql.append(property);
    }
    
    SqlUtils.appendWhereClause(sql, CollectionUtils.getSingleValueCollection(metadata.getIdentifierPropertyName()), 
        new ArrayList<String>(), null, metadata.getPropertyColumnNameMap());
    
    return sql.toString();
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
  
}
